// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package changelist

import (
	"context"
	"fmt"
	"sort"
	"time"

	"go.chromium.org/luci/common/clock"
	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	"go.chromium.org/luci/common/retry/transient"
	"go.chromium.org/luci/gae/service/datastore"

	"go.chromium.org/luci/cv/internal/common"
)

// CL is a CL entity in Datastore.
type CL struct {
	_kind  string                `gae:"$kind,CL"`
	_extra datastore.PropertyMap `gae:"-,extra"`

	// ID is auto-generated by Datastore.
	ID common.CLID `gae:"$id"` // int64
	// ExternalID must not be modified once entity is created.
	ExternalID ExternalID `gae:",noindex"` // string. Indexed in CLMap entities.

	// EVersion is entity version. Every update should increment it by 1.
	// See Update() function.
	EVersion int64 `gae:",noindex"`

	// UpdateTime is exact time of when this entity was last updated.
	//
	// It's not indexed to avoid hot areas in the index.
	UpdateTime time.Time `gae:",noindex"`

	// RetentionKey is for data retention purpose.
	//
	// It is indexed and tries to avoid hot areas in the index. The format is
	// `{shard_key}/{unix_time_of_UpdateTime}`. Shard key is the last 2
	// digit of CLID with left padded zero. Unix timestamp is a 10 digit integer
	// with left padded zero if necessary.
	RetentionKey string

	// Snapshot is the latest known state of a CL. It may be and often is
	// behind the source of truth, which is the code review site (e.g. Gerrit).
	Snapshot *Snapshot

	// ApplicableConfig keeps track of configs applicable to the CL.
	//
	// TODO(tandrii): merge into .Access.
	ApplicableConfig *ApplicableConfig

	// Access records per-LUCI project visibility of a CL.
	//
	// See description in protobuf type with the same name.
	//
	// TODO(tandrii): rename GAE field to `Access`.
	Access *Access `gae:"DependentMeta"`

	// IncompleteRuns tracks not yet finalized Runs working on this CL. Sorted.
	//
	// It's updated transactionally with the Run being modified.
	IncompleteRuns common.RunIDs `gae:",noindex"`

	// TriggerNewPatchsetRunAfterPS indicates the patchset number after which
	// new patchset runs can be triggered.
	//
	// E.g. if this value is set to 2, do not trigger new patchset runs for
	// patchsets 1 or 2. Presumably this is because those runs were already
	// completed/failed/otherwise purged.
	//
	// This is needed because unlike label votes which CV can remove, triggering
	// new patchset upload runs relies on the presence of the patchset in the CL
	// snapshot, which cannot be removed.
	TriggerNewPatchsetRunAfterPS int32 `gae:",noindex"`
}

// clMap is CLMap entity in Datastore which ensures strict 1:1 mapping
// between internal and external IDs.
type clMap struct {
	_kind string `gae:"$kind,CLMap"`

	// ExternalID as entity ID ensures uniqueness.
	ExternalID ExternalID `gae:"$id"` // string
	// InternalID is auto-generated by Datastore for CL entity.
	InternalID common.CLID `gae:",noindex"` // int64. Indexed in CL entities.
}

// URL returns URL of the CL.
func (cl *CL) URL() (string, error) { return cl.ExternalID.URL() }

// ToUpdatedEvent returns CLUpdatedEvent corresponding to the current CL
// version.
func (cl *CL) ToUpdatedEvent() *CLUpdatedEvent {
	return &CLUpdatedEvent{
		Clid:     int64(cl.ID),
		Eversion: cl.EVersion,
	}
}

// DO NOT decrease the shard. It will cause olds CLs that are out of retention
// period in the shard not getting wiped out.
const retentionKeyShards = 100

// UpdateRetentionKey updates the RetentionKey of the CL.
//
// Panics if the CL.ID and/or CL.UpdateTime is absent.
func (cl *CL) UpdateRetentionKey() {
	switch {
	case cl.ID == 0:
		panic(errors.New("clid is not set"))
	case cl.UpdateTime.IsZero():
		panic(errors.New("cl.UpdateTime is not set"))
	}
	cl.RetentionKey = fmt.Sprintf("%02d/%010d", cl.ID%retentionKeyShards, cl.UpdateTime.Unix())
}

// ToUpdatedEvents returns CLUpdatedEvents from a slice of CLs.
func ToUpdatedEvents(cls ...*CL) *CLUpdatedEvents {
	events := make([]*CLUpdatedEvent, len(cls))
	for i, cl := range cls {
		if cl.ID == 0 || cl.EVersion == 0 {
			panic(fmt.Errorf("ID %d and EVersion %d must not be 0", cl.ID, cl.EVersion))
		}
		events[i] = &CLUpdatedEvent{
			Clid:     int64(cl.ID),
			Eversion: cl.EVersion,
		}
	}
	sort.Slice(events, func(i, j int) bool {
		// Assume unique CLIDs.
		return events[i].GetClid() < events[j].GetClid()
	})
	return &CLUpdatedEvents{Events: events}
}

// Load reads a CL from Datastore.
//
// Returns nil, nil if it doesn't exist.
func (eid ExternalID) Load(ctx context.Context) (*CL, error) {
	m := clMap{ExternalID: eid}
	switch err := datastore.Get(ctx, &m); {
	case err == datastore.ErrNoSuchEntity:
		return nil, nil
	case err != nil:
		return nil, transient.Tag.Apply(errors.Fmt("failed to get CLMap: %w", err))
	}
	cl := &CL{ID: m.InternalID}
	switch err := datastore.Get(ctx, cl); {
	case err == datastore.ErrNoSuchEntity:
		// This should not happen in practice except in the case of a very old CL
		// which is being deleted due to retention policy. Log error but return it
		// as transient as it's expected that CLMap entity would be removed soon,
		// and so a retry would be produce proper datastore.ErrNoSuchEntity error.
		msg := fmt.Sprintf("unexpectedly failed to get CL#%d given existing CLMap%q", m.InternalID, eid)
		logging.Errorf(ctx, msg)
		return nil, transient.Tag.Apply(errors.New(msg))
	case err != nil:
		return nil, transient.Tag.Apply(errors.Fmt("failed to get CL: %w", err))
	}
	return cl, nil
}

// MustCreateIfNotExists is for use in tests to ensure CL exists.
//
// Panicks on errors.
func (eid ExternalID) MustCreateIfNotExists(ctx context.Context) *CL {
	// Fast path without transaction.
	if cl, err := eid.Load(ctx); err == nil && cl != nil {
		return cl
	}
	var cl *CL
	err := datastore.RunInTransaction(ctx, func(ctx context.Context) (err error) {
		cl, err = eid.Load(ctx)
		switch {
		case err != nil:
			return err
		case cl != nil:
			return nil
		}
		cl = &CL{
			ExternalID: eid,
			EVersion:   1,
			UpdateTime: datastore.RoundTime(clock.Now(ctx).UTC()),
		}
		if err := datastore.AllocateIDs(ctx, cl); err != nil {
			return err
		}
		cl.UpdateRetentionKey()
		m := clMap{ExternalID: eid, InternalID: cl.ID}
		return datastore.Put(ctx, &m, cl)
	}, nil)

	if err != nil {
		panic(err)
	}
	return cl
}

// Delete deletes CL and its CLMap entities transactionally.
//
// Thus, deletion and insertion (part of ExternalID.getOrInsert) are atomic with
// respect to one another.
//
// However, ExternalID.get and fast path of ExternalID.getOrInsert if called
// concurrently with Delete may return a temporary error, but on retry they would
// return ErrNoSuchEntity.
func Delete(ctx context.Context, id common.CLID) error {
	cl := CL{ID: id}
	switch err := datastore.Get(ctx, &cl); {
	case err == datastore.ErrNoSuchEntity:
		return nil // Nothing to do.
	case err != nil:
		return transient.Tag.Apply(errors.Fmt("failed to get a CL: %w", err))
	}

	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		m := clMap{ExternalID: cl.ExternalID}
		return datastore.Delete(ctx, &cl, &m)
	}, nil)
	if err != nil {
		return transient.Tag.Apply(errors.Fmt("failed to delete a CL: %w", err))
	}
	return nil
}

// Lookup loads CLID for each given ExternalID.
//
// CLID is 0 if ExternalID is not yet known.
// Returns a single error (not MultiError) if there were multiple errors.
func Lookup(ctx context.Context, eids []ExternalID) ([]common.CLID, error) {
	out := make([]common.CLID, len(eids))
	entities := make([]clMap, len(eids))
	for i, eid := range eids {
		entities[i].ExternalID = eid
	}
	err := datastore.Get(ctx, entities)
	merrs, _ := err.(errors.MultiError)
	switch {
	case err == nil:
		for i, e := range entities {
			out[i] = e.InternalID
		}
		return out, nil
	case merrs == nil:
		return nil, transient.Tag.Apply(errors.Fmt("failed to load clMap: %w", err))
	default:
		for i, err := range merrs {
			switch {
			case err == nil:
				out[i] = entities[i].InternalID
			case err != datastore.ErrNoSuchEntity:
				return nil, transient.Tag.Apply(errors.WrapIf(common.MostSevereError(merrs), "failed to load clMap"))
			}
		}
		return out, nil
	}
}
